Azure.Identity adding msi implementation for AppService and CloudShell#6699
Azure.Identity adding msi implementation for AppService and CloudShell#6699schaabs merged 13 commits intoAzure:masterfrom
Conversation
| private SemaphoreSlim _initLock = new SemaphoreSlim(1, 1); | ||
|
|
||
|
|
||
| private readonly Uri ImdsEndptoint = new Uri("http://169.254.169.254/metadata/identity/oauth2/token"); |
There was a problem hiding this comment.
These two constants are probably not needed now that they're in ManagedIdentityClient
| // if ONLY the env var MSI_ENDPOINT is set the MsiType is CloudShell | ||
| else | ||
| { | ||
| s_msiType = MsiType.AppService; |
| // if ONLY the env var MSI_ENDPOINT is set the MsiType is CloudShell | ||
| else | ||
| { | ||
| s_msiType = MsiType.AppService; |
|
|
||
| request.UriBuilder.Uri = ImdsEndpoint; | ||
|
|
||
| request.UriBuilder.AppendQuery("api-version", ImdsApiVersion); |
There was a problem hiding this comment.
It's okay if we get a 400 due to no query string.
There was a problem hiding this comment.
I feel like specifying the API version is good. Possibly future versions won't require the Metadata header. I know that this is unlikely but adding the api-version is very low cost.
There was a problem hiding this comment.
Good point, I'll add it in the Python library as well.
|
|
||
| request.Method = HttpPipelineMethod.Get; | ||
|
|
||
| request.Headers.Add("secret", s_secret); |
There was a problem hiding this comment.
Docs claim the secret rotates. I don't know whether that requires an application restart so I get the value from the environment for each request.
| private Request CreateAppServiceAuthRequest(string[] scopes, string clientId) | ||
| { | ||
| // covert the scopes to a resource string | ||
| string resource = ScopeUtilities.ScopesToResource(scopes); |
There was a problem hiding this comment.
I think the managed identity endpoints require one scope per request.
There was a problem hiding this comment.
ScopesToResource enforces only one scope is specified. I left all the signatures as a string[] to keep it uniform with the outermost api.
public static string ScopesToResource(string[] scopes)
{
if (scopes == null) throw new ArgumentNullException(nameof(scopes));
if (scopes.Length != 1) throw new ArgumentException("To convert to a resource string the specified array must be exactly length 1", nameof(scopes));
KrzysztofCwalina
left a comment
There was a problem hiding this comment.
In general, it looks good. The main top level comment I would have is that the code could use more comments :-)
| { | ||
| var response = await _pipeline.SendRequestAsync(request, cancellationToken).ConfigureAwait(false); | ||
|
|
||
| if (response.Status == 200 || response.Status == 201) |
There was a problem hiding this comment.
ClientOptions has a property called ResponseClassifier. Should it be used here to determine if the code is success? I assume you don't want to do it, but then why do we have ResponseClassifier on options?
There was a problem hiding this comment.
The behavior of this client and AadIdentityClient differ in what codes are failure, and what codes should be retried. Currently we're using the IdentityClientOptions class for both clients but this needs to be changed. I've added issue #6711 to track this.
| return result; | ||
| } | ||
|
|
||
| throw response.CreateRequestFailedException(); |
There was a problem hiding this comment.
Should we add some custom message here? i.e. is there some action we could recommend users take when this fails? The default message just shows the the response line and headers (not even the body, I think).
There was a problem hiding this comment.
We should add an exception type so that these failures can be easily distinguished from service client failures, as both would originate from the same user call to a service client method. I've added issue #6712 to track this.
| } | ||
| } | ||
|
|
||
| return new AccessToken(accessToken, expiresOn); |
There was a problem hiding this comment.
What happens id access_token does not exist in the JSON payload? i.e. is it ok to pass null here?
There was a problem hiding this comment.
If the access_token to not exist here then the Token property on AccessToken will be null and this will be handled higher up in the stack. So while it didn't get a token, it should not throw an exception from here.
| { | ||
| if (cancellationToken != default) | ||
| { | ||
| Task.Delay(1000, cancellationToken).GetAwaiter().GetResult(); |
There was a problem hiding this comment.
@stephentoub, is this reliable? BTW, we are doing it as Thread.Sleep does not have an overload that takes a cancellation token.
There was a problem hiding this comment.
It's "reliable" in that it'll sleep for at least a second. It is effectively sync-over-async, though, in that this will block the current thread until the async operation completes, which necessitates a thread pool thread briefly, so if this were production code and this was running on a thread pool thread, I'd be more concerned.
Thread.Sleep doesn't take a cancellation token as it just maps to the OS' thread sleep functionality, e.g. ::Sleep(milliseconds) on Windows, where there's no notion of cancellation. We could of course add another cancelable overload in the future, at which point we'd not just use the OS functionality but isntead do something like the below...
If this were production code, and you wanted to synchronously block the current thread for X milliseconds or until a cancellation request occurred, I'd probably instead advocate for something more like:
private static readonly ManualResetEventSlim s_sleeper = new ManualResetEventSlim();
...
s_sleeper.Wait(1000, cancellationToken);but for test code, what you have is fine.
| internal class AadClient | ||
| { | ||
| private static Lazy<IdentityClient> s_sharedClient = new Lazy<IdentityClient>(() => new IdentityClient()); | ||
| private static Lazy<AadClient> s_sharedClient = new Lazy<AadClient>(() => new AadClient()); |
There was a problem hiding this comment.
Are there any scenarios under which s_sharedClient is rendered unusable? E.g. a fatal error that it is not possible to recover from without recreating the client instance?
There was a problem hiding this comment.
The client is mostly stateless. The only errors which would render the client useless would be through miss configuration of the process, which would mean the s_sharedClient would never be usable and error on all calls.
| { | ||
| var response = await _pipeline.SendRequestAsync(request, cancellationToken).ConfigureAwait(false); | ||
|
|
||
| if (response.Status == 200 || response.Status == 201) |
There was a problem hiding this comment.
Out of curiosity, under what circumstances would we get a 201 response?
There was a problem hiding this comment.
This logic needs to be moved into the ClientOptions. I've added issue #6711 to track this.
| { | ||
| s_endpoint = new Uri(endpointEnvVar); | ||
|
|
||
| // if BOTH the env vars MSI_ENDPOINT and MSI_SECRET are set the MsiType is AppService |
There was a problem hiding this comment.
What environment variables do aks/aci/acs use?
| // this indicates that the request timed out and that imds is not available. Otherwise the operation | ||
| // was user cancelled. In this case we don't wan't to handle the exception so s_identityAvailable will | ||
| // remain unset, as we still haven't determined if the imds endpoint is available. | ||
| catch (OperationCanceledException ex) when (ex.CancellationToken == imdsTimeout) |
There was a problem hiding this comment.
can you check cancellationToken.IsCancellationRequested?
No description provided.